1. application.properties
關閉Redis相關設定
---省略其他設定---
#L2 Cache 關閉 (Redis)
redis.l2.cache.enabled=false
# 關閉 Spring Data Redis repositories 功能,避免不必要的 Redis 相關功能被啟用
spring.data.redis.repositories.enabled=false
# 指定 Session 儲存庫為 none,表示不使用任何 Session 儲存庫,適用於不需要 Session 管理的應用程式
spring.session.store-type=none
開啟Redis相關設定
---省略其他設定---
# 當 Redis 開啟時 (L1 + L2 模式) ---
# 建議同時設定 Session 刷新模式(由 Redis 索引管理)
redis.l2.cache.enabled=true
# 指定 Session 儲存庫為 Redis
spring.session.store-type=redis
# 使用 Redis 索引管理 Session 刷新,確保 Session 在 Redis 中的有效性和一致性
spring.session.redis.repository-type=indexed
# Redis 連線設定
redis_host=redis.lewishome.tw
redis_port=6379
# 若沒有實作加密機制,則直接使用明文密碼(不建議)
redis_password=${KEYSTORE:redis_password}
**調整後的 application.properties **
#各類資料庫的 url 參考:
# H2 database ==> jdbc:h2:mem:testdb
# SQL-Server ==> jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]]
# example ==> jdbc:sqlserver://sql.lewis-home.tw:1433;databasename=MBS;encrypt=false
# MySQL ==> jdbc:mysql://[hosts][:portNumber][/database]
# example ==> jdbc:mysql://mysql.lewis-home.tw:33060
# AS400(Jt400)==> jdbc:as400://[hosts][;property=value]
# example ==> jdbc:as400://as400system;transaction isolation=none;translate binary=true;date format=iso;prompt=false
# Oracle ==> jdbc:oracle:thin:@[HOST][:PORT]:SID or jdbc:oracle:thin:@//[HOST][:PORT]/SERVICE
# example ==> jdbc:oracle:thin:@oracle.lewis-home.tw:1521:oracle.lewis-home.tw
# postgres ==> jdbc:postgresql://@[netloc][:port][/dbname][?param1=value1&...]
# example ==> jdbc:postgresql://postsql.lewis-home.tw:5432/database
#各類資料庫的 driver class Name
# H2 database ==> org.h2.Driver
# SQL-Server ==> com.microsoft.sqlserver.jdbc.SQLServerDriver
# MySQL ==> com.mysql.jdbc.Driver
# AS400(Jt400)==> com.ibm.as400.access.AS400JDBCDriver
# Oracle ==> oracle.jdbc.driver.OracleDriver
# postgres ==> org.postgresql.Driver
#Store primary Datasource (這些是自訂的變數名稱,只要與程式內取用的設定一致即可)
primary.datasource.enabled=true
primary.datasource.jdbcurl=jdbc:sqlserver://sql.lewishome.tw:1433;databasename=DbMuser1;encrypt=false;characterEncoding=utf-8
primary.datasource.username=Muser1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
# Encrypted in KeyStore with alias: primary_datasource_password
primary.datasource.password=${KEYSTORE:primary_datasource_password}
primary.datasource.driver_class_name=com.microsoft.sqlserver.jdbc.SQLServerDriver
primary.datasource.hibernate.hbm2ddl.auto=update
#Secondary Datasource 是否有需要
secondary.datasource.enabled=true
# secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/pdb1
secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/orclpdb.lewishome.tw
secondary.datasource.username=OUSER1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
# Encrypted in KeyStore with alias: secondary_datasource_password
secondary.datasource.password=${KEYSTORE:secondary_datasource_password}
secondary.datasource.driver_class_name=oracle.jdbc.OracleDriver
secondary.datasource.hibernate.hbm2ddl.auto=update
# secondary.datasource.hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# # CSRF protection whitelist - add /doMenu endpoint
# csrf.endpoint.white-list=/jwtAuth/**;/systemApiTest/**;/opt/fonts/**;{base}/callback/**;/home;/doMenu
# mail_resource_jndi=java=jboss/mail/mailSmtp
mail.smtp.host=mail.lewis-home.tw
mail.smtp.port=25
mail.sender=lewis@lewis-home.tw
mail.sender_name=lewis.yang
mail.subject.prefix=[WebAppSystem]
# #L2 Cache 關閉 (Redis)
# redis.l2.cache.enabled=false
# # 關閉 Spring Data Redis repositories 功能,避免不必要的 Redis 相關功能被啟用
# spring.data.redis.repositories.enabled=false
# # 指定 Session 儲存庫為 none,表示不使用任何 Session 儲存庫,適用於不需要 Session 管理的應用程式
# spring.session.store-type=none
# 當 Redis 開啟時 (L1 + L2 模式) ---
# 建議同時設定 Session 刷新模式(由 Redis 索引管理)
redis.l2.cache.enabled=true
# 指定 Session 儲存庫為 Redis
spring.session.store-type=redis
# 使用 Redis 索引管理 Session 刷新,確保 Session 在 Redis 中的有效性和一致性
spring.session.redis.repository-type=indexed
# Redis 連線設定
redis_host=redis.lewishome.tw
redis_port=6379
# 若沒有實作加密機制,則直接使用明文密碼(不建議)
redis_password=${KEYSTORE:redis_password}
2. WebappApplication.java
位置: tw.lewishome.webapp;
---省略其他設定---
@SpringBootApplication(exclude = {
// 當Redis開關關閉時,我們不希望 Spring Session 去碰 Redis
RedisAutoConfiguration.class
,RedisRepositoriesAutoConfiguration.class
// ,SessionAutoConfiguration.class
})
**調整後的 WebappApplication.java **
package tw.lewishome.webapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import lombok.extern.slf4j.Slf4j;
/**
* WebappApplication 是 Spring Boot 應用程式的主要入口點。
* 此類別負責啟動應用程式並加載 Spring 上下文。
*
* @author Lewis
* @version 1.0
*/
@SpringBootApplication(exclude = {
// 當Redis開關關閉時,我們不希望 Spring Session 去碰 Redis
RedisAutoConfiguration.class
,RedisRepositoriesAutoConfiguration.class
// ,SessionAutoConfiguration.class
})
@Slf4j
public class WebappApplication {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new AsyncServiceWorkerSample instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
private WebappApplication() {
// This constructor is intentionally empty. Nothing special is needed here.
}
/**
* 主方法是應用程式的進入點。
* 此方法啟動 Spring 應用程式。
*
* @param args 命令列參數
*/
public static void main(String[] args) {
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ SpringApplication WebappApplication started! │");
log.info("└────────────────────────────────────────────────────┘");
SpringApplication springApplication = new SpringApplication(WebappApplication.class);
// 手動添加 EnvironmentPostProcessor,以確保在應用程式啟動時處理安全屬性(如加密的密碼)
springApplication.addInitializers(new PropertySecureProcessInitializer(
new PropertySecureProcessor(),
new PropertySecureConverter()));
springApplication.run(args);
}
}
3. CachePolicy.java
-位置: package tw.lewishome.webapp.base.cache;
package tw.lewishome.webapp.base.cache;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* 集中定義系統中所有快取空間的名稱與過期時間
*/
public class CachePolicy {
// 統一管理快取名稱字串,避免 typo
public static final String USER_MENU_ITEMS = "catchUserMenuItems";
public static final String USER_AUTH_SEQ = "catchUserAuthSeq";
public static final String SHORT_LIVED = "shortLivedCache";
/**
* L1 (Caffeine) 的特定配置
* 回傳 Map<CacheName, TTL_Minutes>
*/
public static Map<String, Integer> getL1CustomPolicies() {
Map<String, Integer> policies = new HashMap<>();
policies.put(USER_MENU_ITEMS, 60); // 60 分鐘
policies.put(USER_AUTH_SEQ, 3); // 3 分鐘 (對應原本的預設)
policies.put(SHORT_LIVED, 1); // 1 分鐘
return policies;
}
/**
* L2 (Redis) 的特定配置
* 回傳 Map<CacheName, Duration>
*/
public static Map<String, Duration> getL2CustomPolicies() {
Map<String, Duration> policies = new HashMap<>();
policies.put(USER_MENU_ITEMS, Duration.ofMinutes(30));
policies.put(USER_AUTH_SEQ, Duration.ofHours(12));
return policies;
}
}
4. CaffeineConfiguration.java
-位置: package tw.lewishome.webapp.base.cache.caffeine;
package tw.lewishome.webapp.base.cache.caffeine;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.interceptor.CacheInterceptor;
import org.springframework.cache.interceptor.CacheOperationSource;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Role;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.cache.CachePolicy;
import tw.lewishome.webapp.base.cache.redis.MultiCacheInterceptor;
/**
* Caffeine 快取設定(CaffeineConfiguration)。
*
* 主要功能:
* 1. 初始化 L1 本地快取 (Caffeine)。
* 2. 針對不同快取區塊設定不同 TTL。
* 3. 註冊自定義 CacheInterceptor 以支援 L1+L2 聯動。
*/
@EnableCaching
@Configuration
@Slf4j
public class CaffeineConfiguration {
/**
* 建立 CacheManager 並針對特定 Cache 名稱設定不同的 TTL
*/
@Bean(name = "caffeineCacheManager")
@Primary
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public static CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>();
// 從 Policy 類別循環加入自定義 TTL 的快取
CachePolicy.getL1CustomPolicies().forEach((name, ttlMinutes) -> {
caches.add(createCaffeineCache(name, ttlMinutes));
});
cacheManager.setCaches(caches);
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ [L1 Cache] CaffeineCacheManager 初始化完成... │");
log.info("└────────────────────────────────────────────────────┘");
return cacheManager;
}
/**
* 重要:註冊自定義攔截器以取代預設行為。
* 這會讓標註為 @Cacheable(..., cacheManager="redisCacheManager") 的方法
* 自動先去查詢 caffeineCacheManager (L1)。
*/
@Bean
@Primary
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public static CacheInterceptor customerCacheInterceptor(
CacheOperationSource cacheOperationSource,
//這裡要加 @Qualifier避免使用錯誤的CacheManager
@Qualifier("caffeineCacheManager") CacheManager caffeineCacheManager,
ObjectProvider<CacheManager> allManagersProvider) {
MultiCacheInterceptor interceptor = new MultiCacheInterceptor();
interceptor.setCacheOperationSources(cacheOperationSource);
interceptor.setCaffeineCacheManager(caffeineCacheManager);
// 透過型別探測:找出不是 Caffeine」的那個 Manager
CacheManager redisManager = allManagersProvider.orderedStream()
.filter(cacheManager -> cacheManager != caffeineCacheManager)
.findFirst()
.orElse(null);
if (redisManager == null) {
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ [Cache] L2 Redis 已關閉,僅使用 L1 (Caffeine) │");
log.info("└────────────────────────────────────────────────────┘");
} else {
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ [Cache] L1+L2 (Caffeine + Redis) 聯動模式已啟動 │");
log.info("└────────────────────────────────────────────────────┘");
}
return interceptor;
}
/**
* 輔助方法:根據分鐘數建立獨立的 Cache 實例
*/
private static CaffeineCache createCaffeineCache(String name, int ttlMinutes) {
return new CaffeineCache(name, Caffeine.newBuilder()
.expireAfterWrite(ttlMinutes, TimeUnit.MINUTES)
.maximumSize(1000)
.recordStats()
.build());
}
}
4. RedisConfiguration.java
-位置: package tw.lewishome.webapp.base.cache.redis;
package tw.lewishome.webapp.base.cache.redis;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
// import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisIndexedHttpSession;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.cache.CachePolicy;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
import tw.lewishome.webapp.base.utility.common.TypeConvert;
/**
* Redis RedisConfiguration
* 負責設定 Redis 連線與相關快取管理 Bean。
*
* 主要功能:
* 1. 初始化 L2 Redis快取
* 2. 支援多重 TTL 配置,根據 CachePolicy 定義不同快取區塊的 TTL。
* 3. 提供 RedisTemplate 以支援非註解式的 Redis 操作。
* 4. 從環境變數讀取 Redis 連線配置,確保靈活性與安全性。
* 5. 使用 @ConditionalOnProperty 控制 Redis 快取的啟用
*
* @author Lewis
* @since 2024
*/
@Configuration
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) // 加入這一行,保護配置類本身 不被掃描為一般 Bean
@ConditionalOnProperty(name = "redis.l2.cache.enabled", havingValue = "true")
public class RedisConfiguration {
@Autowired
SystemEnvReader systemEnvReader;
public RedisConfiguration() {
// Default constructor
}
/**
* 建立 Redis 快取管理器 (L2 Cache)。
*
* @param redisConnectionFactory Redis 連線工廠
* @return CacheManager 返回配置完成的 RedisCacheManager
*/
@Bean(name = "redisCacheManager")
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ redisCacheManager with Multi-TTL initializing... │");
log.info("└────────────────────────────────────────────────────┘");
// 1. 取得預設 TTL (預設 1 小時)
String redisTimeToLife = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_LIFE_HOUR","1") : "1";
int defaultHours = TypeConvert.toInteger(redisTimeToLife);
// 2. 基本配置:使用 JSON 序列化並設定預設 TTL
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(defaultHours))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 3. 建立並回傳 RedisCacheManager,包含自定義的多重 TTL 配置
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(getL2Configurations(defaultConfig))
.build();
}
// 根據 CachePolicy 定義的多重 TTL 建立對應的 RedisCacheConfiguration
private Map<String, RedisCacheConfiguration> getL2Configurations(RedisCacheConfiguration defaultConfig) {
Map<String, RedisCacheConfiguration> configurations = new HashMap<>();
// 從 Policy 類別循環加入配置
CachePolicy.getL2CustomPolicies().forEach((name, ttl) -> {
configurations.put(name, defaultConfig.entryTtl(ttl));
});
return configurations;
}
/**
* 建立 LettuceConnectionFactory 連線工廠。
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public LettuceConnectionFactory redisConnectionFactory() {
try {
RedisStandaloneConfiguration redisStandaloneConfiguration = getRedisStandaloneConfiguration();
return new LettuceConnectionFactory(redisStandaloneConfiguration);
} catch (Exception ex) {
log.error("CRITICAL: Failed to create RedisConnectionFactory: {}", ex.getMessage(), ex);
return null;
}
}
/**
* 建立標準 RedisTemplate 以進行非註解式的 Redis 操作。
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
/**
* 從 SystemEnvReader 讀取環境變數並初始化 Redis 獨立連線配置。
*/
private RedisStandaloneConfiguration getRedisStandaloneConfiguration() {
String redisHostName = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_HOST","redis.lewishocme.tw") : "redis.lewishome.tw";
String redisPort = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_PORT","637s9") : "6379";
String redisHostPassword = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_PASSWORD","XXXXXXX") : "XXXXXXXX";
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHostName, TypeConvert.toInteger(redisPort));
config.setPassword(RedisPassword.of(redisHostPassword));
return config;
}
}
**5. 調整SystemEnvReader.java
-位置:package tw.lewishome.webapp.base.utility.common;
-因為系統開機時有以下警告訊息:
trationDelegate$BeanPostProcessorChecker : Bean 'systemEnvReader' of type [tw.lewishome.webapp.base.utility.common.SystemEnvReader] is not eligible for getting processed by all BeanPostProcessors
(for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [meterRegistryPostProcessor]?
Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
所以依建議,將SystemEnvReader.java宣告為ROLE_INFRASTRUCTURE
@Component
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SystemEnvReader {
--省略程式邏輯 --
}
6. 使用Catchable方式
@Cacheable(cacheNames = CachePolicy.USER_AUTH_SEQ, key = "#userId")
public List<String> getSysUserLastAuth(String userId) {
-- 省略程式邏輯 --
}